using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using MinimizeEngine.Components.Render;
using MinimizeEngine.Components.Render.Grid_System;


namespace MinimizeEngine.Collision
{
    /// <summary>
    /// This is a game component that implements IUpdateable.
    /// </summary>
    public static class MECollisionFunctions
    {
        /// <summary>
        /// Check to see if a Ray Intersects a Bounding Sphere
        /// </summary>
        /// <param name="ray"></param>
        /// <returns></returns>
        public static bool RayIntersects(Ray ray, BoundingSphere boundingSphere)
        {
            if (boundingSphere.Intersects(ray) != null)
            {
                return true;
            }
            return false;
        }

        /// <summary>
        /// Check to see if a Ray Intersects with a Model.
        /// We need to be able to access the Models Vertex Data, and so we use a
        /// Custom Model Processor when loading the Models.
        /// This returns the distance along the Ray to the Point of Intersection,
        /// or null if there is no Intersection.
        /// </summary>
        public static float? RayIntersectsModel(Ray ray, MERenderComponent renderComponent, out bool insideBS, out Vector3 v1,
                                                out Vector3 v2, out Vector3 v3)
        {
            //Set all Vertices to Zero
            v1 = v2 = v3 = Vector3.Zero;

            //As the Ray we are using is in World Space, and our Model is in Object Space,
            //we have to transfer one of the two into the same Space. As a Model may
            //contain a lot of Triangles, we shall transfer the Ray into Object space. This
            //can speed up the detection by a vast amount.
            Matrix inverseTranform = Matrix.Invert(renderComponent.WorldTransformMatrix);

            ray.Position = Vector3.Transform(ray.Position, inverseTranform);
            ray.Direction = Vector3.TransformNormal(ray.Direction, inverseTranform);

            //Get the Custom Data from the Model when we loaded it in.
            Dictionary<string, object> modelTagData = (Dictionary<string, object>)renderComponent.Model.Tag;

            //Throw an Exception if the Tag Data is null, meaning we did not store
            //the Vertex Data when loading in.
            if (modelTagData == null)
            {
                throw new InvalidOperationException("Tag Data of the Model has not been set properly." +
                                                        "Please make sure that all Models are built using the Custom Model Processor!");
            }

            //Do a quick Bounding Sphere Test to begin with.
            BoundingSphere bs = (BoundingSphere)modelTagData["BoundingSphere"];

            //Check to see if the Ray Intersects with the Bounding Sphere.
            //If it does not, then we do not have to progress onto the more complex check.
            if (bs.Intersects(ray) == null)
            {
                insideBS = false;

                return null;
            }
            else
            {
                //We are in the Bounding Sphere
                insideBS = true;

                //Now we need to do a proper Triangle Collision Test.
                float? closestPOI = null;

                //First we loop through all of the Vertex Data.
                //We do this 3 at a time, as a Triangle is made up of 3 Vertices.
                Vector3[] modelVertices = (Vector3[])modelTagData["Vertices"];

                for (int i = 0; i < modelVertices.Length; i += 3)
                {
                    //Point of Intersection
                    float? poi;

                    //Do a Ray to Triangle Intersection Test
                    RayIntersectsTriangle(ref ray, ref modelVertices[i], ref modelVertices[i + 1],
                                            ref modelVertices[i + 2], out poi);

                    //Check to see if the Ray does Intersect with the Triangle
                    if (poi != null)
                    {
                        //Check to see if Intersection is closer than any of the previous Intersection
                        if ((closestPOI == null) || (poi < closestPOI))
                        {
                            //Store the new Closes Intersection
                            closestPOI = poi;

                            //Now we transform the Vertex Positions into World Space.
                            //We store them in the output parameters of this function.
                            Matrix worldTrans = renderComponent.WorldTransformMatrix;
                            Vector3.Transform(ref modelVertices[i], ref worldTrans, out v1);
                            Vector3.Transform(ref modelVertices[i + 1], ref worldTrans, out v2);
                            Vector3.Transform(ref modelVertices[i + 2], ref worldTrans, out v3);
                        }
                    }
                }
                return closestPOI;
            }
        }

        /// <summary>
        /// Helper function to check if a Ray Intersects with a Triangle
        /// </summary>
        /// <param name="ray">Ray to check against</param>
        /// <param name="v1">Vertex 1</param>
        /// <param name="v2">Vertex 2</param>
        /// <param name="v3">Vertex 3</param>
        /// <param name="result">Result of the Check (Point of Intersection)</param>
        public static void RayIntersectsTriangle(ref Ray ray, ref Vector3 v1, ref Vector3 v2,
                                            ref Vector3 v3, out float? result)
        {
            //Triangle Edges
            Vector3 edge1, edge2;

            //Create Vectors along two Edges of the Triangle
            Vector3.Subtract(ref v2, ref v1, out edge1);
            Vector3.Subtract(ref v3, ref v1, out edge2);

            //Compute the Determinant;
            Vector3 crossEdge2;
            float determinant;
            Vector3.Cross(ref ray.Direction, ref edge2, out crossEdge2);
            Vector3.Dot(ref edge1, ref crossEdge2, out determinant);

            //We know that if the Ray is Parallel to the Triangles Plane, then there can be no Collision.
            if (determinant > -float.Epsilon && determinant < float.Epsilon)
            {
                result = null;
                return;
            }

            //Now get the Inverse of the Determinant
            float inverseDeterminant = 1.0f / determinant;

            //Now calculate the U Parameter of the Point of Intersection
            Vector3 distance;
            float triangleU;
            Vector3.Subtract(ref ray.Position, ref v1, out distance);
            Vector3.Dot(ref distance, ref crossEdge2, out triangleU);
            triangleU *= inverseDeterminant;

            //Now check to make sure that we are inside of the Triangle
            if (triangleU < 0 || triangleU > 1)
            {
                result = null;
                return;
            }

            //Now calculate the V Parameter of the Point of Intersection
            Vector3 crossEdge1;
            float triangleV;
            Vector3.Cross(ref distance, ref edge1, out crossEdge1);
            Vector3.Dot(ref ray.Direction, ref crossEdge1, out triangleV);
            triangleV *= inverseDeterminant;

            //Again check that we are still inside of the Triangle
            if (triangleV < 0 || triangleU + triangleV > 1)
            {
                result = null;
                return;
            }

            //Now calculate the Distance along the Ray to the Triangle.
            float rayDistance;
            Vector3.Dot(ref edge2, ref crossEdge1, out rayDistance);
            rayDistance *= inverseDeterminant;

            //Check to see if the Triangle is behind the Rays Origin.
            if (rayDistance < 0)
            {
                result = null;
                return;
            }

            //Out the Rays Distance.
            result = rayDistance;
        }

        /// <summary>
        /// Get the Radius of a given Model
        /// </summary>
        /// <returns>Returns the Radius of the given Model</returns>
        public static float GetModelRadius(Model model)
        {
            Vector3 modelCenter = Vector3.Zero;
            float modelRadius;
            Matrix[] mModelTransforms;

            mModelTransforms = new Matrix[model.Bones.Count];
            model.CopyAbsoluteBoneTransformsTo(mModelTransforms);

            foreach (ModelMesh mesh in model.Meshes)
            {
                BoundingSphere meshBounds = mesh.BoundingSphere;
                Matrix transform = mModelTransforms[mesh.ParentBone.Index];
                Vector3 meshCenter = Vector3.Transform(meshBounds.Center, transform);

                modelCenter += meshCenter;
            }

            modelCenter /= model.Meshes.Count;

            // Now we know the center point, we can compute the model radius  
            // by examining the radius of each mesh bounding sphere.  
            modelRadius = 0;

            foreach (ModelMesh mesh in model.Meshes)
            {
                BoundingSphere meshBounds = mesh.BoundingSphere;
                Matrix transform = mModelTransforms[mesh.ParentBone.Index];
                Vector3 meshCenter = Vector3.Transform(meshBounds.Center, transform);

                float transformScale = transform.Forward.Length();

                float meshRadius = (meshCenter - modelCenter).Length() +
                                   (meshBounds.Radius * transformScale);

                modelRadius = Math.Max(modelRadius, meshRadius);
            }

            return modelRadius;
        }
    }
}